package com.vipshop.compass.context;

import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.vipshop.compass.common.StringUtil;
import com.vipshop.compass.config.GlobalConfigurationException;
import com.vipshop.compass.config.Globals;

/**
 * This class is a factory which used to provide the datasource
 * 
 * @author Levin Cai
 * 
 */
public class DataSourceFactory {
	/** data source types */
	public enum DS_TYPE {
		c3p0, jdbc, jndi
	}

	// for c3p0 data source
	private static final String DATASOURCE_C3P0_ACQUIRE_INCREMENT = "com.vipshop.datasource.{0}.{1}.acquireIncrement";
	private static final String DATASOURCE_C3P0_ACQUIRE_RETRY_ATTEMPTS = "com.vipshop.datasource.{0}.{1}.acquireRetryAttempts";
	private static final String DATASOURCE_C3P0_ACQUIRE_RETRY_DELAY = "com.vipshop.datasource.{0}.{1}.acquireRetryDelay";
	private static final String DATASOURCE_C3P0_AUTO_COMMIT_ON_CLOSE = "com.vipshop.datasource.{0}.{1}.autoCommitOnClose";
	private static final String DATASOURCE_C3P0_AUTOMATIC_TEST_TABLE = "com.vipshop.datasource.{0}.{1}.automaticTestTable";
	private static final String DATASOURCE_C3P0_BREAK_AFTER_ACQUIRE_FAILURE = "com.vipshop.datasource.{0}.{1}.breakAfterAcquireFailure";
	private static final String DATASOURCE_C3P0_CHECKOUT_TIMEOUT = "com.vipshop.datasource.{0}.{1}.checkoutTimeout";
	private static final String DATASOURCE_C3P0_IDLE_CONNECTION_TEST_PERIOD = "com.vipshop.datasource.{0}.{1}.idleConnectionTestPeriod";
	private static final String DATASOURCE_C3P0_INITIAL_POOL_SIZE = "com.vipshop.datasource.{0}.{1}.initialPoolSize";
	private static final String DATASOURCE_C3P0_MAX_ADMINISTRATIVE_TASK_TIME = "com.vipshop.datasource.{0}.{1}.maxAdministrativeTaskTime";
	private static final String DATASOURCE_C3P0_MAX_CONNECTION_AGE = "com.vipshop.datasource.{0}.{1}.maxConnectionAge";
	private static final String DATASOURCE_C3P0_MAX_IDLE_TIME = "com.vipshop.datasource.{0}.{1}.maxIdleTime";
	private static final String DATASOURCE_C3P0_MAX_IDLE_TIME_EXCESS_CONNECTIONS = "com.vipshop.datasource.{0}.{1}.maxIdleTimeExcessConnections";
	private static final String DATASOURCE_C3P0_MAX_STATEMENTS = "com.vipshop.datasource.{0}.{1}.maxStatements";
	private static final String DATASOURCE_C3P0_MAX_STATEMENTS_PERCONNECTION = "com.vipshop.datasource.{0}.{1}.maxStatementsPerConnection";
	private static final String DATASOURCE_C3P0_NUMHELPER_THREADS = "com.vipshop.datasource.{0}.{1}.numHelperThreads";

	private static final String DATASOURCE_DIRVER = "com.vipshop.datasource.{0}.{1}.driverClassName";
	private static final String DATASOURCE_JDBC_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED = "com.vipshop.datasource.{0}.{1}.accessToUnderlyingConnectionAllowed";
	private static final String DATASOURCE_JDBC_DEFAULT_AUTOCOMMIT = "com.vipshop.datasource.{0}.{1}.defaultAutoCommit";
	private static final String DATASOURCE_JDBC_LOGIN_TIMEOUT = "com.vipshop.datasource.{0}.{1}.loginTimeout";
	private static final String DATASOURCE_JDBC_MAX_OPEN_STATEMENTS = "com.vipshop.datasource.{0}.{1}.maxOpenStatements";
	private static final String DATASOURCE_JDBC_MAXIDLE = "com.vipshop.datasource.{0}.{1}.maxIdle";

	private static final String DATASOURCE_JDBC_MAXWAIT = "com.vipshop.datasource.{0}.{1}.maxWait";
	private static final String DATASOURCE_JDBC_MIN_EVICTABLEIDLE_TIME_MILLIS = "com.vipshop.datasource.{0}.{1}.minEvictableIdleTimeMillis";
	private static final String DATASOURCE_JDBC_MINIDLE = "com.vipshop.datasource.{0}.{1}.minIdle";
	private static final String DATASOURCE_JDBC_POOL_PREPARED_STATEMENTS = "com.vipshop.datasource.{0}.{1}.poolPreparedStatements";
	// for apache basic data source
	private static final String DATASOURCE_JDBC_TIMEOUT = "com.vipshop.datasource.{0}.{1}.timeout";
	private static final String DATASOURCE_JNDI_NAME = "com.vipshop.datasource.{0}.jndiName";
	private static final String DATASOURCE_LIST_ENTRY_NAME = "com.vipshop.datasource.list";
	private static final String DATASOURCE_MAX_POOL_SIZE = "com.vipshop.datasource.{0}.{1}.maxPoolSize";
	private static final String DATASOURCE_MIN_POOL_SIZE = "com.vipshop.datasource.{0}.{1}.minPoolSize";
	private static final String DATASOURCE_PASSWORD = "com.vipshop.datasource.{0}.{1}.password";
	private static final String DATASOURCE_TYPE = "com.vipshop.datasource.{0}.type";
	// common configuration for all types
	private static final String DATASOURCE_URL = "com.vipshop.datasource.{0}.{1}.url";
	private static final String DATASOURCE_USERNAME = "com.vipshop.datasource.{0}.{1}.username";
	/** datasources need to be instanced */
	private static List<String> dataSourceNameList = new ArrayList<String>();

	private static DataSourceFactory dsFactorySingleton;;

	private static Log logger = LogFactory.getLog(DataSourceFactory.class);

	/** data source list */
	private static Map<String, DataSource> validDataSourceMap = new HashMap<String, DataSource>();

	/**
	 * Get the DataSourceFactory singleton instance.
	 * 
	 * @return dsFactorySingleton
	 * @throws GlobalConfigurationException
	 */
	public static DataSourceFactory getInstance() throws GlobalConfigurationException {
		if (dsFactorySingleton == null) {
			dsFactorySingleton = new DataSourceFactory();
		}
		return dsFactorySingleton;
	}

	public DataSourceFactory() throws GlobalConfigurationException {
		initConfiguration();
		loadDataSources();
	}

	public void clearInstance() {
		// Clear data sources
		for (Map.Entry<String, DataSource> entry : dsFactorySingleton.validDataSourceMap.entrySet()) {
			DataSource dataSource = entry.getValue();
			if (dataSource instanceof ComboPooledDataSource) {
				ComboPooledDataSource c3p0DataSource = (ComboPooledDataSource) dataSource;
				c3p0DataSource.close();
			} else if (dataSource instanceof BasicDataSource) {
				BasicDataSource dbcpDataSource = (BasicDataSource) dataSource;
				try {
					dbcpDataSource.close();
				} catch (SQLException e) {
					logger.error("Error closing Apache DBCP connection", e);
				}
			}
		}

		// Clear instance
		dsFactorySingleton = null;
	}

	/**
	 * Load the C3P0 data sources
	 * 
	 * @param dsName
	 * @param dsType
	 * @throws GlobalConfigurationException
	 */
	private void createC3P0DataSource(String dsName, String dsType) throws GlobalConfigurationException {
		logger.info("start to load c3p0 dataSources: " + dsName);

		String url = getEntryValue(DATASOURCE_URL, dsName, dsType);
		String driverClassName = getEntryValue(DATASOURCE_DIRVER, dsName, dsType);
		String username = getEntryValue(DATASOURCE_USERNAME, dsName, dsType);
		String password = getEntryValue(DATASOURCE_PASSWORD, dsName, dsType);
		String minPoolSize = getEntryValue(DATASOURCE_MIN_POOL_SIZE, dsName, dsType);
		String maxPoolSize = getEntryValue(DATASOURCE_MAX_POOL_SIZE, dsName, dsType);
		String timeout = getEntryValue(DATASOURCE_JDBC_TIMEOUT, dsName, dsType);

		ComboPooledDataSource c3p0PooledDataSource = new ComboPooledDataSource();
		// required properties
		try {
			c3p0PooledDataSource.setDriverClass(driverClassName);
		} catch (PropertyVetoException e) {
			throw new GlobalConfigurationException("DataSourceFactory: C3P0 did not like the driver class '" + driverClassName + "' specified in the global properties at '" + MessageFormat.format(DATASOURCE_DIRVER, dsName, dsType) + "'.", e);
		}
		c3p0PooledDataSource.setJdbcUrl(url);
		c3p0PooledDataSource.setUser(username);
		c3p0PooledDataSource.setPassword(password);

		// optional properties

		String idleConnectionTestPeriod = getEntryValue(DATASOURCE_C3P0_IDLE_CONNECTION_TEST_PERIOD, dsName, dsType);
		if (!StringUtil.isEmpty(idleConnectionTestPeriod))
			c3p0PooledDataSource.setIdleConnectionTestPeriod(Integer.parseInt(idleConnectionTestPeriod));

		String initialPoolSize = getEntryValue(DATASOURCE_C3P0_INITIAL_POOL_SIZE, dsName, dsType);
		if (!StringUtil.isEmpty(initialPoolSize))
			c3p0PooledDataSource.setInitialPoolSize(Integer.parseInt(initialPoolSize));

		String acquireIncrement = getEntryValue(DATASOURCE_C3P0_ACQUIRE_INCREMENT, dsName, dsType);
		if (!StringUtil.isEmpty(acquireIncrement))
			c3p0PooledDataSource.setAcquireIncrement(Integer.parseInt(acquireIncrement));

		String acquireRetryAttempts = getEntryValue(DATASOURCE_C3P0_ACQUIRE_RETRY_ATTEMPTS, dsName, dsType);
		if (!StringUtil.isEmpty(acquireRetryAttempts))
			c3p0PooledDataSource.setAcquireRetryAttempts(Integer.parseInt(acquireRetryAttempts));

		String acquireRetryDelay = getEntryValue(DATASOURCE_C3P0_ACQUIRE_RETRY_DELAY, dsName, dsType);
		if (!StringUtil.isEmpty(acquireRetryDelay))
			c3p0PooledDataSource.setAcquireRetryDelay(Integer.parseInt(acquireRetryDelay));

		String autoCommitOnClose = getEntryValue(DATASOURCE_C3P0_AUTO_COMMIT_ON_CLOSE, dsName, dsType);
		if (!StringUtil.isEmpty(autoCommitOnClose))
			c3p0PooledDataSource.setAutoCommitOnClose(Boolean.parseBoolean(autoCommitOnClose));

		String automaticTestTable = getEntryValue(DATASOURCE_C3P0_AUTOMATIC_TEST_TABLE, dsName, dsType);
		if (!StringUtil.isEmpty(automaticTestTable))
			c3p0PooledDataSource.setAutomaticTestTable(automaticTestTable);

		String breakAfterAcquireFailure = getEntryValue(DATASOURCE_C3P0_BREAK_AFTER_ACQUIRE_FAILURE, dsName, dsType);
		if (!StringUtil.isEmpty(breakAfterAcquireFailure))
			c3p0PooledDataSource.setBreakAfterAcquireFailure(Boolean.parseBoolean(breakAfterAcquireFailure));

		String checkoutTimeout = getEntryValue(DATASOURCE_C3P0_CHECKOUT_TIMEOUT, dsName, dsType);
		if (!StringUtil.isEmpty(checkoutTimeout))
			c3p0PooledDataSource.setCheckoutTimeout(Integer.parseInt(checkoutTimeout));

		String maxAdministrativeTaskTime = getEntryValue(DATASOURCE_C3P0_MAX_ADMINISTRATIVE_TASK_TIME, dsName, dsType);
		if (!StringUtil.isEmpty(maxAdministrativeTaskTime))
			c3p0PooledDataSource.setMaxAdministrativeTaskTime(Integer.parseInt(maxAdministrativeTaskTime));

		String maxConnectionAge = getEntryValue(DATASOURCE_C3P0_MAX_CONNECTION_AGE, dsName, dsType);
		if (!StringUtil.isEmpty(maxConnectionAge))
			c3p0PooledDataSource.setMaxConnectionAge(Integer.parseInt(maxConnectionAge));

		String maxIdleTime = getEntryValue(DATASOURCE_C3P0_MAX_IDLE_TIME, dsName, dsType);
		if (!StringUtil.isEmpty(maxIdleTime))
			c3p0PooledDataSource.setMaxIdleTime(Integer.parseInt(maxIdleTime));

		String maxIdleTimeExcessConnections = getEntryValue(DATASOURCE_C3P0_MAX_IDLE_TIME_EXCESS_CONNECTIONS, dsName, dsType);
		if (!StringUtil.isEmpty(maxIdleTimeExcessConnections))
			c3p0PooledDataSource.setMaxIdleTimeExcessConnections(Integer.parseInt(maxIdleTimeExcessConnections));

		String maxStatements = getEntryValue(DATASOURCE_C3P0_MAX_STATEMENTS, dsName, dsType);
		if (!StringUtil.isEmpty(maxStatements))
			c3p0PooledDataSource.setMaxStatements(Integer.parseInt(maxStatements));

		String maxStatementsPerConnection = getEntryValue(DATASOURCE_C3P0_MAX_STATEMENTS_PERCONNECTION, dsName, dsType);
		if (!StringUtil.isEmpty(maxStatementsPerConnection))
			c3p0PooledDataSource.setMaxStatementsPerConnection(Integer.parseInt(maxStatementsPerConnection));

		String numHelperThreads = getEntryValue(DATASOURCE_C3P0_NUMHELPER_THREADS, dsName, dsType);
		if (!StringUtil.isEmpty(numHelperThreads))
			c3p0PooledDataSource.setNumHelperThreads(Integer.parseInt(numHelperThreads));

		logger.info("c3p0 dataSources " + dsName + " is loaded.");
		testDataSource(c3p0PooledDataSource);
		validDataSourceMap.put(dsName, c3p0PooledDataSource);
	}

	/**
	 * Load the apache basic data source
	 * 
	 * @param dsName
	 * @param dsType
	 * @throws GlobalConfigurationException
	 */
	private void createJDBCDataSource(String dsName, String dsType) throws GlobalConfigurationException {
		logger.info("start to load basic jdbc dataSources: " + dsName);

		// required properties
		BasicDataSource basicDataSource = new BasicDataSource();
		basicDataSource.setUrl(getEntryValue(DATASOURCE_URL, dsName, dsType));
		basicDataSource.setDriverClassName(getEntryValue(DATASOURCE_DIRVER, dsName, dsType));
		basicDataSource.setUsername(getEntryValue(DATASOURCE_USERNAME, dsName, dsType));
		basicDataSource.setPassword(getEntryValue(DATASOURCE_PASSWORD, dsName, dsType));

		// optional properties
		String minPoolSize = getEntryValue(DATASOURCE_MIN_POOL_SIZE, dsName, dsType);
		if (!StringUtil.isEmpty(minPoolSize))
			basicDataSource.setInitialSize(Integer.parseInt(minPoolSize));

		String maxPoolSize = getEntryValue(DATASOURCE_MAX_POOL_SIZE, dsName, dsType);
		if (!StringUtil.isEmpty(maxPoolSize))
			basicDataSource.setMaxActive(Integer.parseInt(maxPoolSize));

		String timeout = getEntryValue(DATASOURCE_JDBC_TIMEOUT, dsName, dsType);
		if (!StringUtil.isEmpty(timeout))
			basicDataSource.setMaxWait(Integer.parseInt(timeout));

		String poolPreparedStatements = getEntryValue(DATASOURCE_JDBC_POOL_PREPARED_STATEMENTS, dsName, dsType);
		if (!StringUtil.isEmpty(poolPreparedStatements))
			basicDataSource.setPoolPreparedStatements(Boolean.parseBoolean(poolPreparedStatements));

		String accessToUnderlyingConnectionAllowed = getEntryValue(DATASOURCE_JDBC_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED, dsName, dsType);
		if (!StringUtil.isEmpty(accessToUnderlyingConnectionAllowed))
			basicDataSource.setAccessToUnderlyingConnectionAllowed(Boolean.parseBoolean(accessToUnderlyingConnectionAllowed));

		String maxIdle = getEntryValue(DATASOURCE_JDBC_MAXIDLE, dsName, dsType);
		if (!StringUtil.isEmpty(maxIdle))
			basicDataSource.setMaxIdle(Integer.parseInt(maxIdle));

		String defaultAutoCommit = getEntryValue(DATASOURCE_JDBC_DEFAULT_AUTOCOMMIT, dsName, dsType);
		if (!StringUtil.isEmpty(defaultAutoCommit))
			basicDataSource.setDefaultAutoCommit(Boolean.parseBoolean(defaultAutoCommit));

		String loginTimeout = getEntryValue(DATASOURCE_JDBC_LOGIN_TIMEOUT, dsName, dsType);
		if (!StringUtil.isEmpty(loginTimeout))
			try {
				basicDataSource.setLoginTimeout(Integer.parseInt(loginTimeout));
			} catch (NumberFormatException e) {
				throw new GlobalConfigurationException(e);
			} catch (SQLException e) {
				throw new GlobalConfigurationException(e);
			}

		String maxOpenStatements = getEntryValue(DATASOURCE_JDBC_MAX_OPEN_STATEMENTS, dsName, dsType);
		if (!StringUtil.isEmpty(maxOpenStatements))
			basicDataSource.setMaxOpenPreparedStatements(Integer.parseInt(maxOpenStatements));

		String maxWait = getEntryValue(DATASOURCE_JDBC_MAXWAIT, dsName, dsType);
		if (!StringUtil.isEmpty(maxWait))
			basicDataSource.setMaxWait(Integer.parseInt(maxWait));

		String minEvictableIdleTimeMillis = getEntryValue(DATASOURCE_JDBC_MIN_EVICTABLEIDLE_TIME_MILLIS, dsName, dsType);
		if (!StringUtil.isEmpty(minEvictableIdleTimeMillis))
			basicDataSource.setMinEvictableIdleTimeMillis(Integer.parseInt(minEvictableIdleTimeMillis));

		String minIdle = getEntryValue(DATASOURCE_JDBC_MINIDLE, dsName, dsType);
		if (!StringUtil.isEmpty(minIdle))
			basicDataSource.setMinIdle(Integer.parseInt(minIdle));

		testDataSource(basicDataSource);
		validDataSourceMap.put(dsName, basicDataSource);

		logger.info("basic jdbc dataSources: " + dsName + " is created!");
	}

	/**
	 * Load the JNDI data sources
	 * 
	 * @param dsName
	 * @throws GlobalConfigurationException
	 */
	private void createJNDIDataSource(String dsName) throws GlobalConfigurationException {
		logger.info("start to load JNDI dataSources: " + dsName);
		DataSource dataSource = null;
		String jndiName = Globals.getInstance().getProperty(MessageFormat.format(DATASOURCE_JNDI_NAME, dsName));
		try {
			dataSource = (DataSource) new InitialContext().lookup(jndiName);
		} catch (NamingException e) {
			throw new GlobalConfigurationException();
		}
		testDataSource(dataSource);
		validDataSourceMap.put(dsName, dataSource);
		logger.info("JNDI dataSources " + dsName + " is loaded.");
	}

	public DataSource getDataSource(String dsName) throws GlobalConfigurationException {
		DataSource ds = getInstance().validDataSourceMap.get(dsName);
		if (ds == null) {
			throw new GlobalConfigurationException("No such a datasource is defined in com.vipshop.datasource.list.");
		}
		return ds;
	}

	private String getEntryValue(String keyTemp, String dsName, String dsType) {
		return Globals.getInstance().getProperty(MessageFormat.format(keyTemp, dsName, dsType));
	}

	/**
	 * initialize the properties from the globals file.
	 */
	private void initConfiguration() {
		String dsListStr = Globals.getInstance().getProperty(DATASOURCE_LIST_ENTRY_NAME);
		String[] dsArray = dsListStr.split(",");
		for (String ds : dsArray) {
			if (StringUtil.isEmpty(ds)) {
				continue;
			}
			dataSourceNameList.add(ds);
		}
	}

	/**
	 * initialize the data source list according to the data source name list
	 * 
	 * @throws GlobalConfigurationException
	 */
	private void loadDataSources() throws GlobalConfigurationException {
		logger.info("-->start to load dataSources.");
		for (String dsName : dataSourceNameList) {
			String typeKey = MessageFormat.format(this.DATASOURCE_TYPE, dsName);
			String typeValue = Globals.getInstance().getProperty(typeKey);
			if (typeValue.equalsIgnoreCase(DS_TYPE.jdbc.toString())) {
				createJDBCDataSource(dsName, typeValue);
			} else if (typeValue.equalsIgnoreCase(DS_TYPE.c3p0.toString())) {
				createC3P0DataSource(dsName, typeValue);
			} else if (typeValue.equalsIgnoreCase(DS_TYPE.jndi.toString())) {
				createJNDIDataSource(dsName);
			}
		}
		logger.info("-->all dataSources are loaded.");
	}

	private void testDataSource(DataSource ds) throws GlobalConfigurationException {
		if (ds == null) {
			logger.error("datasource is null.");
			throw new GlobalConfigurationException("Invalid datasource.");
		}
		Connection conn = null;
		try {
			conn = ds.getConnection();
			if (conn == null) {
				logger.error("Connection is null!");
				throw new GlobalConfigurationException("Invalid datasource.");
			}
		} catch (SQLException e) {
			throw new GlobalConfigurationException(e);
		} finally {
			if (conn != null) {
				try {
					conn.close();
				} catch (SQLException e) {
					throw new GlobalConfigurationException(e);
				}
			}
		}
	}

}
